// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;

namespace SnapObjects.Data
{
    internal class SqlSyntaxParser
    {
        protected readonly ISqlGenerationHelper _helper;
        protected readonly ITypeMapper _typeMapper;

        public SqlSyntaxParser(ISqlGenerationHelper helper, ITypeMapper typeMapper)
        {
            _helper = helper;
            _typeMapper = typeMapper;
        }

        public string SqlStatement { get; private set; }

        public IReadOnlyDictionary<string, IAdoDbParameter> SqlParameters { get; private set; }

        public void Parse(string sqlText, Func<bool, string> nameGenerator = null)
        {
            this.Parse(sqlText, new object[] { }, true, nameGenerator);
        }

        public void Parse(string sqlText, Func<bool, string> nameGenerator, object[] values)
        {
            this.Parse(sqlText, values, false, nameGenerator);
        }

        private void Parse(
            string sqlText, object[] values, bool ignoreSetParamValue, Func<bool, string> nameGenerator)
        {
            var parameters = new Dictionary<string, IAdoDbParameter>(StringComparer.OrdinalIgnoreCase);

            var tokenizer = new SqlSyntaxTokenizer(sqlText);

            var tokens = tokenizer.Tokens;

            for (var tokenIndex = 0; tokenIndex < tokens.Count; tokenIndex++)
            {
                var isParameter = false;
                var name = String.Empty;
                var token = tokens[tokenIndex];

                if (this.IsParameter(token))
                {
                    isParameter = true;
                    name = token.Length > 1 ? token.Substring(1) : "P" + parameters.Count;
                }

                if (!isParameter)
                {
                    continue;
                }

                if (!parameters.TryGetValue(name, out var adoDbParameter))
                {
                    adoDbParameter = this.CreateParameter(
                        parameters.Count, values, name, nameGenerator, ignoreSetParamValue);

                    if (adoDbParameter != null)
                    {
                        parameters.Add(name, adoDbParameter);
                    }
                }

                tokens[tokenIndex] = adoDbParameter.ParameterName;
            }

            this.SqlParameters = parameters;
            this.SqlStatement = String.Join("", tokens);
        }

        private bool IsParameter(string token)
        {
            if (token.Length > 1)
            {
                var firstChar = token[0];
                var secondChar = token[1];

                if (Equals(firstChar, '@') ||
                    Equals(firstChar, ':') ||
                    Equals(firstChar, _helper.ParameterIdentifier))
                {
                    return firstChar != secondChar;
                }
            }
            else if (token.Length == 1 && ('?'.Equals(token[0]) || _helper.IsParameterPlaceholder(token[0])))
            {
                return true;
            }

            return false;
        }

        private IAdoDbParameter CreateParameter(
            int index, object[] values, string name, Func<bool, string> nameGenerator, bool ignoreSetParamValue)
        {
            var pname = nameGenerator == null ?
                _helper.GenerateParameterName(name) :
                _helper.GenerateParameterName(nameGenerator(false));

            if (ignoreSetParamValue)
            {
                return new AdoDbParameter(pname, null, null);
            }

            if (values == null || index >= values.Length)
            {
                throw new ArgumentException("The number of parameters does not match.");
            }

            var sqlParams = values
                .OfType<ParamValue>()
                .ToDictionary(m => m.Name, m => m, StringComparer.OrdinalIgnoreCase);

            if (sqlParams.Count > 0)
            {
                if (sqlParams.TryGetValue(name, out var param))
                {
                    return new AdoDbParameter(pname, param.Value, param.DataType, param.Direction);
                }
                else
                {
                    throw new ArgumentException($"No parameter '{name}' found.");
                }
            }
            else
            {
                var type = values[index] != null ? values[index].GetType() : typeof(object);

                return new AdoDbParameter(pname, values[index], type, ParameterDirection.Input);
            }
        }
    }
}
